package com.czh.subject.common.utils;

import lombok.Getter;

/**
 * 生成唯一id工具类
 * @author czh01
 * @since 2024/9/11
 */
public class IdWorkerUtil {
    /**
     * 雪花算法(Snowflake)是一种分布式ID生成算法，旨在生成唯一的长整型ID
     * 该算法将ID分为以下几个部分：
     * - 1位符号位：始终为0
     * - 41位时间戳（毫秒级），减去某个固定时间戳（twepoch）
     * - 10位机器ID，包括数据中心ID（datacenterId）和工作机器ID（workerId）
     * - 12位序列号（sequence），用于在同一个毫秒内生成不同的ID
     */
    @Getter
    private final long workerId; // 工作机器ID

    @Getter
    private final long datacenterId; // 数据中心ID

    // 序列号，用于在同一个毫秒内产生不同的ID
    private long sequence;

    // 时间戳起始点，这是一个固定的毫秒时间戳
    private final long twepoch = 1585644268888L;

    // 工作机器ID所占的位数
    private final long workerIdBits = 5L;

    // 数据中心ID所占的位数
    private final long datacenterIdBits = 5L;

    // 序列号所占的位数
    private final long sequenceBits = 12L;

    // 工作机器ID的最大值
    private final long maxWorkerId = ~(-1L << workerIdBits);

    // 数据中心ID的最大值
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);

    // 工作机器ID的左移位数
    private final long workerIdShift = sequenceBits;

    // 数据中心ID的左移位数
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    // 时间戳的左移位数
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 序列号的最大值掩码
    private final long sequenceMask = ~(-1L << sequenceBits);

    // 上一个时间戳，用于防止同一个毫秒内产生重复的ID
    private long lastTimestamp = -1L;


    /**
     * @param workerId     工作机器ID，用于区分不同机器产生的ID
     * @param datacenterId 数据中心ID，用于区分不同数据中心产生的ID
     * @param sequence     序列号，用于产生ID时保证其唯一性
     */
    public IdWorkerUtil(long workerId, long datacenterId, long sequence) {
        // 校验workerId是否在合法范围内
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(
                    String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        // 校验datacenterId是否在合法范围内
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(
                    String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    /**
     * 生成下一个ID
     * 该方法通过同步方式确保在同一时间戳内生成的ID具有唯一性
     * 它结合了时间戳、数据中心ID、工作机器ID和序列号来生成全局唯一的ID
     *
     * @return 生成的唯一ID
     * @throws RuntimeException 如果时钟回拨，拒绝生成ID
     */
    public synchronized long nextId() {
        // 获取当前时间戳
        long timestamp = timeGen();

        // 检查时钟是否回拨，如果回拨则抛出异常
        if (timestamp < lastTimestamp) {
            System.err.printf(
                    "clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        // 如果当前时间戳与上一次时间戳相同
        if (lastTimestamp == timestamp) {
            // 序列号自增并进行位与操作确保序列号不超过最大值
            sequence = (sequence + 1) & sequenceMask;
            // 如果序列号溢出，则等待下一个毫秒时间戳
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 如果当前时间戳与上一次时间戳不同（即新的一毫秒），重置序列号
            sequence = 0;
        }
        // 更新上一次时间戳为当前时间戳
        lastTimestamp = timestamp;

        // 结合时间戳、数据中心ID、工作机器ID和序列号生成最终的ID并返回
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }

    /**
     * 获取下一个时间戳
     * 此方法用于确保生成的时间戳总是递增的
     * 它通过不断生成新的时间戳直到得到一个大于最后时间戳的值
     *
     * @param lastTimestamp 最后的时间戳，这是需要被下一个时间戳超过的时间点
     * @return 返回一个新的时间戳，这个时间戳大于最后时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        // 不断生成新的时间戳直到它大于最后的时间戳
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 获取时间戳
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }
}